<!--
  - 迭代自适应加权惩罚最小二乘 AirpLS
 -->
 
<!-- 视图层 --><!-- 页头(上) -->
<template><header-comp />

<!-- 功能简介 -->
<uni-card title="功能简介">
  <!-- 通告栏 -->
  <uni-notice-bar text=
    "To新手：
    　　读取数据后，直接去最下面点“绘图预览”。
    　　再根据谱图情况，微调“airPLS算法调参”下的“惩罚系数”。"
  />

  <!-- 文字 -->
  <view><text>
    　　迭代自适应加权惩罚最小二乘法（Adaptive Iterative Reweighted Penalized Least Squares, airPLS）。\n
    　　发展自惩罚最小二乘法（Penalized Least Squares, PLS），并借鉴了非对称最小二乘法（Asymmetric Least Square）、加权最小二乘法（Weighted Least Squares）和迭代重加权最小二乘法（Iteratively Reweighted Least Squares）。\n
    　　其主要思路为：\n
    　　① 最初会生成与[数据点]完全一致的一组[拟合点]，并生成全为“1”的一组[权重]。\n
    　　② 然后计算得到[拟合项]和[惩罚项]：[拟合项]为所有([拟合点]与[数据点]的差的平方，再乘以每个点的[权重])的加和；[惩罚项]为所有(新[拟合点]与旧[拟合点]的差的平方)的加和，再乘以惩罚系数。\n
    　　③ 每次迭代，均会通过最小二乘法求解[拟合项 + 惩罚项]的最小值，解得最优的一组新[权重]、一组新[拟合点]。\n
    　　④ 迭代刚开始，为降低[惩罚项]，处于峰位的[拟合点]会下降，处于基线位/谷位的[拟合点]会上升，会出现很多高于[数据点]的[拟合点]。[拟合点]其实就是拟合基线，因此这会导致很多“负峰”出现——这是不被允许的。故airPLS算法将[拟合点]超过[数据点]的部分的加和作为“损失值”予以监控。\n
    　　⑤ 随着迭代优化的进行，处于峰位的点的[权重]也会降低，其在直接降低[拟合项 + 惩罚项]的同时，也会引起[拟合点]进一步下移至基线位/谷位的[数据点]处，“损失值”也会波动下降。最终：迭代收敛（两次迭代的“损失值”变化率小于1%）；或迭代至预设的最大迭代次数；或迭代至“损失值”低于设定的损失容忍值，则结束迭代，得到最终[拟合点]，即为基线。\n
    　　该算法无需预先进行峰检测等操作，且参数较少，是对波谱谱图进行基线拟合、基线扣除的有效方法。
  </text></view>

  <!-- 二级标题：参考文献 -->
  <uni-section
    title="参考文献"
    type="circle"
    padding="0 1em"
  >
    <!-- 文字内容：文献 -->
    <view><text>
      　　文献链接：
    </text></view>
    <!-- 链接：文献 -->
    <uni-link
      text="https://sci.bban.top/pdf/10.1039/B922045C.pdf"
      href="https://sci.bban.top/pdf/10.1039/B922045C.pdf"
      class="center" color="blue"
    />
    <!-- 文字内容：库 -->
    <view><text>
      　　库链接：
    </text></view>
    <!-- 链接：库 -->
    <uni-link
      text="https://www.npmjs.com/package/ml-airpls"
      href="https://www.npmjs.com/package/ml-airpls"
      class="center" color="blue"
    />
  </uni-section>

</uni-card>

<!-- 读取数据 -->
<uni-card title="读取数据">
  <!-- 通告栏 -->
  <uni-notice-bar text=
    "打开含有波谱数据集的Excel文件。"
  />

  <!-- 二级标题：快速开始 -->
  <uni-section
    v-if="!isGetXlsxFile"
    title="快速开始"
    type="circle"
    padding="0 1em"
  >
    <!-- 文字内容 -->
    <view><text>
      　　一键读取示例数据，快速体验功能。
    </text></view>
    <!-- 读取示例数据按钮 -->
    <view class="center">
      <button
        @click="readExampleData()"
        size="mini" type="primary"
      >
        快速读取示例数据
      </button>
    </view>
  </uni-section>

  <!-- 二级标题：下载示例数据文件 -->
  <uni-section
    title="示例数据下载"
    type="circle"
    padding="0 1em"
  >
    <!-- 文字内容 -->
    <view><text>
      　　操作步骤：\n
      　　① 下载示例Excel模板文件。\n
      　　② 根据模板格式，将你自己的数据拷贝至模板。\n
      　　③ 保存模板文件，然后直接点“点击按钮读取数据文件”读取你的数据文件即可。
    </text></view>
    <!-- 示例文件下载按钮 -->
    <view class="center">
      <button
        @click="downloadExampleFile()"
        size="mini" type="primary"
      >
        示例Excel文件下载
      </button>
    </view>
  </uni-section>

  <!-- 二级标题：读取数据文件 -->
  <uni-section
    title="读取数据"
    type="circle"
    padding="0 1em"
  >
    <!-- 文件上传容器 -->
    <uni-file-picker
      title="　　点击按钮读取数据文件："
      file-extname="xlsx, xls" fileMediatype="all"
      limit="1" return-type="object" :del-icon="false"
      :auto-upload="false" :disable-preview="true"
      @select="(event) => readDataFile(event)"
    />
  </uni-section>

  <!-- 二级标题：数据形状 -->
  <!-- 条件渲染：得到数据才能渲染 -->
  <uni-section
    v-if="isGetXlsxFile"
    title="数据形状"
    type="circle"
    padding="0 1em"
  >
    <!-- 文字内容 -->
    <view><text>
      　　需确定是“正置谱图”还是“倒置谱图”。\n
      　　紫外光谱为典型的正置谱图；红外光谱为典型的倒置谱图。\n
      　　默认“正置”。
    </text></view>
    <!-- 数据形状 -->
    <view class="center">
      倒置　<switch
        :checked="isMaxCriteria"
        @change="(event) => { isMaxCriteria = event.detail.value }"
        type="switch"
      />　正置
    </view>
  </uni-section>
  
</uni-card>

<!-- 数据预处理。条件渲染：有数据才渲染 -->
<data-prep-comp
  v-if="isGetXlsxFile"
  :isMinToZero="dataPrepProps.isMinToZero"
  :isMinZoom="dataPrepProps.isMinZoom"
  :isMaxToOne="dataPrepProps.isMaxToOne"
  :changeValueFunc="dataPrepProps.changeValueFunc"
/>

<!-- 折叠面板。条件渲染：有数据才渲染 -->
<uni-collapse v-if="isGetXlsxFile">

<!-- airPLS算法调参 -->
<uni-card title="airPLS算法调参">
  <!-- 通告栏 -->
  <uni-notice-bar text=
    "airPLS算法的参数微调，有惩罚系数、容忍因子、最大迭代次数。"
  />
  <!-- 下拉框容器 -->
  <uni-collapse-item title="下拉查看设置详情">

    <!-- 二级标题：调参界面 -->
    <uni-section
      title="调参界面"
      type="circle"
      padding="0 1em"
    >
      <!-- 文字内容 -->
      <view><text>
        　　airPLS算法可设置的参数有:\n
        　　惩罚系数。最重要的指标，即“权重”，计算“惩罚项”时的权重值。默认“100”。\n
        　　最大迭代次数。即当计算始终不收敛时，迭代至多少次即结束计算。默认“30”次。\n
        　　容忍率(%)。会对所有Y轴数据求和，乘以容忍率后得到容忍值，当“损失值”收敛至容忍值以内时，认为拟合结果可接受。当数据不收敛，修改最大迭代次数也没用的时候，可适当以1%为单位增大容忍率。默认“0”。
      </text></view>
      <!-- 输入框的表格容器 -->
      <uni-table
        :border="true" :stripe="true"
      >
        <!-- 标题行 -->
        <uni-tr>
          <uni-th align="center">
            键
          </uni-th>
          <uni-th align="center">
            值
          </uni-th>
        </uni-tr>
        <!-- 内容行：惩罚系数 -->
        <uni-tr>
          <uni-td align="center">
            惩罚系数
          </uni-td>
          <uni-td>
            <uni-easyinput
              v-model="lambda"
              type="number"
              placeholder="请输入惩罚系数"
              trim="all" :clearable="false"
            />
          </uni-td>
        </uni-tr>
        <!-- 内容行：最大迭代次数 -->
        <uni-tr>
          <uni-td align="center">
            最大迭代次数
          </uni-td>
          <uni-td>
            <uni-easyinput
              v-model="maxIterations"
              type="number"
              placeholder="请输入最大迭代次数"
              trim="all" :clearable="false"
            />
          </uni-td>
        </uni-tr>
        <!-- 内容行：容忍率 -->
        <uni-tr>
          <uni-td align="center">
            容忍率(%)
          </uni-td>
          <uni-td>
            <uni-easyinput
              v-model="tolerance"
              type="number"
              placeholder="请输入容忍率"
              trim="all" :clearable="false"
            />
          </uni-td>
        </uni-tr>
      </uni-table>
    </uni-section>

  </uni-collapse-item>
</uni-card>

<!-- 结果栏 -->
<uni-card title="结果栏">
  <!-- 通告栏 -->
  <uni-notice-bar text=
    "预览操作的结果，并导出结果。
    可反复修改参数预览，最后导出自己满意的结果。"
  />

  <!-- 二级标题：结果预览 -->
  <uni-section
    title="结果预览"
    type="circle"
    padding="0 1em"
  >
    <!-- 文字内容 -->
    <view><text>
      　　用第一组波谱数据进行试处理，并对处理结果进行绘图预览。\n
    </text></view>
    <!-- 绘图预览相关按钮容器 -->
    <view class="center">
      <!-- 绘图预览按钮 -->
      <button
        @click="previewResult()"
        size="mini" type="primary"
      >
        绘图预览
      </button>
      <!-- 打开图表板按钮 -->
      <button
        @click="showVisor()"
        size="mini" type="primary"
      >
        打开图表板
      </button>
    </view>
  </uni-section>

  <!-- 二级标题：数据批量处理 -->
  <uni-section
    title="数据批量处理"
    type="circle"
    padding="0 1em"
  >
    <!-- 文字内容 -->
    <view><text>
      　　根据各项设置，批量处理全部波谱数据，并将数据结果导出为Excel文件。\n
      　　所导出的文件格式与输入文件格式一致，分为“去基线”、“基线”、“参数”3个表。
    </text></view>
    <!-- 执行操作按钮 -->
    <view class="center">
      <button
        @click="resultToDownload()"
        size="mini" type="primary"
      >
        一键导出
      </button>
    </view>
  </uni-section>


  <!-- 二级标题：计算历程。条件渲染：存在记录才渲染 -->
  <uni-section
    v-if="logArrRef[0]"
    title="结果记录"
    type="circle"
    padding="0 1em"
  >
    <!-- 计算历程：表格容器 -->
    <uni-table border stripe>
      <!-- 表格标题行 -->
      <uni-tr>
        <uni-th align="center">序号</uni-th>
        <uni-th>内容</uni-th>
      </uni-tr>
      <!-- 表格内容行 -->
      <uni-tr v-for="(log, index) in logArrRef" :key="index">
        <uni-td align="center">{{ index + 1 }}</uni-td>
        <uni-td><text>{{ log }}</text></uni-td>
      </uni-tr>
    </uni-table>
  </uni-section>

</uni-card>

</uni-collapse>

<!-- 页脚(下) -->
<footer-comp /></template>

<!-- 逻辑层 -->
<script setup>
// 导入vue的框架方法
import { shallowRef, ref } from "vue"
// 导入数据预处理组件
import dataPrepComp from "../index/data-prep-comp.vue"
// 封装一个数据预处理组件的传参对象
const dataPrepProps = {
  isMinToZero: false,
  isMinZoom: false,
  isMaxToOne: false,
  changeValueFunc: (key, value) => {
    dataPrepProps[key] = value
  }
}
// 导入tfjs-visor库
// import * as tf from "@tensorflow/tfjs"
// import * as tfvis from "@tensorflow/tfjs-vis"
// 导入xlsx库
import * as XLSX from "xlsx"
// 导入ml库的自适应迭代重加权惩罚最小二乘法
import airpls from "ml-airpls"
// 导入公共方法库的方法
import {
  // 下载文件
  downloadFile,
  // 数组转置
  arrayTranspose,
  // 打开图表栏
  showVisor,
  // 下载示例文件
  downloadExampleFile,
  // 读取XLSX文件为工作簿对象
  readXlsxFile,
  // 从工作表提取原始数据
  sheetToDataArr,
  // 用tfvis渲染Aoa数据为图表
  aoaTotfvisChart,
  // 数据归一化
  minToZero, maxToOne
} from "../../scripts/app_common.js"
// 导入示例数据集
import { exampleData } from "../../datasets/example-data.js"

/**
 * @ref对象
 */
// 是否读取到数据的boolean对象
const isGetXlsxFile = shallowRef(null)
// 谱图正置或倒置的boolean对象
const isMaxCriteria = shallowRef(true)
// airPLS算法的3个number对象
const lambda = shallowRef(100)
const maxIterations = shallowRef(30)
const tolerance = shallowRef(0)
// 训练记录对象
const logArrRef = ref([])

/**
 * @其他对象
 */
// 原始数据整理后得到的数据数组
let dataJsonArr = null

/**
 * @readDataFile 读取数据文件
 * @function async
 * @param { Event } event 点击事件
 * 读取文件并转置，然后调用文件数据检查修复方法solveRawData()
 * 成功后绘图，并修改isGetXlsxFile状态
 */
async function readDataFile(event) { try {
  // 显示加载圈
  uni.showLoading({ title: "正在读取文件数据" })
  // 先清零dataJsonArr数据
  dataJsonArr = null
  // 从event中获取工作簿对象
  const workbook = await readXlsxFile(event.tempFiles[0].file)
  // 将工作簿第1张表转为Json化的数据数组
  dataJsonArr = sheetToDataArr(workbook.Sheets[workbook.SheetNames[0]])
  // 如果数据检查成功通过，接到返回数据
  if (dataJsonArr) {
    // 拿第一个数据作图
    aoaTotfvisChart(
      // 图表类型
      "scatterplot",
      // 图表容器
      { tab: "airPLS", name: "第一组数据" },
      // 图表数据：第一个数据
      [dataJsonArr[0]],
      // 数据序列标签
      ["原始数据"]
    )
    // 操作结束，isGetXlsxFile的ref对象标记为true
    isGetXlsxFile.value = true
  }
  // 隐藏加载圈
  uni.hideLoading()
} catch (error) {
  console.error("readDataFile()报错: ", error)
  uni.hideLoading()
  uni.showModal({
    showCancel: false,
    title: "读取数据文件报错",
    content: error,
  })
}}

/**
 * @readExampleData 直接读取示例数据
 * @function
 */
function readExampleData() { try {
  // 把示例数据组装成AOA形式，直接赋值给dataJsonArr即可
  dataJsonArr = [exampleData]
  // 拿第一个数据作图
  aoaTotfvisChart(
    // 图表类型
    "scatterplot",
    // 图表容器
    { tab: "airPLS", name: "示例数据" },
    // 图表数据：第一个数据
    [dataJsonArr[0]],
    // 数据序列标签
    ["原始数据"]
  )
  // 打开图表栏
  showVisor()
  // 修改isGetXlsxFile状态
  isGetXlsxFile.value = true
} catch (error) {
  console.error("readExampleData()报错: ", error)
  uni.showModal({
    showCancel: false,
    title: "读取示例数据报错",
    content: error,
  })
}}

/**
 * @dataProcess 数据处理，相当于本业务内部的公共方法
 * 会读取页面ref对象的参数，对数据进行处理
 * @function
 * @param {{
 *   x: Number[],
 *   y: Number[],
 *   header: String | Number
 * }} dataJson 单组数据对象
 * @returns {{
      baseline: Number[],
      corrected: Number[],
      iteration: Number,
      error: Number,
 }} 返回的结果，含基线、校正、迭代次数、误差
 */
function dataProcess(dataJson) { try {
  // 深拷贝dataJson对象的y
  const yDeepCopy = JSON.parse(JSON.stringify(dataJson.y))
  // 预处理：零位校正
  if (dataPrepProps.isMinToZero) {
    minToZero(yDeepCopy, dataPrepProps.isMinZoom, isMaxCriteria.value)
  }
  // 预处理：上限归一
  if (dataPrepProps.isMaxToOne) {
    maxToOne(yDeepCopy, isMaxCriteria.value)
  }
  // airPLS处理
  const airplsResult = airpls(dataJson.x, yDeepCopy, {
    // 惩罚系数
    lambda: Number(lambda.value),
    // 容忍度在库里有2个参数，这里都赋值传参
    tolerance: Number(tolerance.value) / 100,
    factorCriterion: Number(tolerance.value) / 100,
    // 最大迭代次数，须为整数
    maxIterations: Math.round(maxIterations.value)
  })
  // 得把没做airPLS处理的y加进去
  airplsResult.unairplsed = yDeepCopy
  // 处理完毕，返回airpls处理结果
  return airplsResult
} catch (error) {
  console.error("dataProcess()报错: ", error)
  uni.showModal({
    showCancel: false,
    title: "数据运算报错",
    content: error,
  })
}}

/**
 * @previewResult 绘图预览
 * @function
 * @note 直接或间接读取页面ref参数，串联所有操作方法
 */
function previewResult() { try {
  // 显示加载圈
  uni.showLoading({ title: "正在处理数据，请稍候..." })
  // 用第一条数据简单处理一下
  const airplsResult = dataProcess(dataJsonArr[0])
  // 确定成功接到处理结果数据
  if (!airplsResult) {
    // 没接收到？那就退出
    // 隐藏加载圈
    uni.hideLoading()
    return
  }
  // 要求和了
  let unairplsedSum = 0
  for (let i = 0; i < airplsResult.unairplsed.length; i++) {
    unairplsedSum = unairplsedSum + airplsResult.unairplsed[i];
  }
  // 更新日志
  if (logArrRef.value.length >= 10) {
    logArrRef.value.shift()
  }
  logArrRef.value.push(`完成迭代过程：
    初始数据加和 = ${ unairplsedSum }
    容忍值 = ${ unairplsedSum * tolerance.value / 100 }
    迭代次数 = ${ airplsResult.iteration }
    损失值 = ${ Math.abs(airplsResult.error) }
  `)
  // 绘图预览
  aoaTotfvisChart(
    // 图表类型
    "scatterplot",
    // 图表容器
    { tab: "airPLS", name: "数据预览(第1组)" },
    // 图表数据
    [
      { x: dataJsonArr[0].x, y: airplsResult.unairplsed },
      { x: dataJsonArr[0].x, y: airplsResult.baseline },
      { x: dataJsonArr[0].x, y: airplsResult.corrected },
    ],
    // 数据序列标签
    ["处理前", "基线", "校正"]
  )
  // 最后，打开图表版呈现数据
  showVisor()
  // 隐藏加载圈
  uni.hideLoading()
} catch (error) {
  console.error("previewResult()报错: ", error)
  uni.hideLoading()
  uni.showModal({
    showCancel: false,
    title: "结果预览报错",
    content: error,
  })
}}

/**
 * @resultToDownload 干到结果并下载
 * 执行处理操作，结果作为Excel表格下载
 * @function
 * @note 直接或间接读取页面ref参数，串联所有操作方法
 */
function resultToDownload() { try {
  // 显示加载圈
  uni.showLoading({ title: "正在处理数据，请稍候..." })
  // 建个接数据的数组
  const baselineAoa = []
  const correctedAoa = []
  const resultArgsAoa = [["数据标签", "迭代次数", "拟合差值"]]
  for (let i = 0; i < dataJsonArr.length; i++) {
    // 处理数据，生成结果
    const airplsResult = dataProcess(dataJsonArr[i])
    // 得判断成功搞到数据了
    if (!airplsResult) {
      // 没搞到数据？直接跳出
      uni.hideLoading()
      return
    }
    // 得判断，是第一个值，或x和前面的不一样
    if ((i === 0) || (dataJsonArr[i].x !== dataJsonArr[i - 1].x)) {
      // 基线
      baselineAoa.push(["X"].concat(Array.from(dataJsonArr[i].x)))
      baselineAoa.push([dataJsonArr[i].header].concat(Array.from(airplsResult.baseline)))
      // 校正
      correctedAoa.push(["X"].concat(Array.from(dataJsonArr[i].x)))
      correctedAoa.push([dataJsonArr[i].header].concat(Array.from(airplsResult.corrected)))
      // 拟合参数
      resultArgsAoa.push([
        dataJsonArr[i].header,
        airplsResult.iteration,
        airplsResult.error
      ])
    // 如果不是第一个值，而且x和前面一样
    } else {
      // 基线
      baselineAoa.push([dataJsonArr[i].header].concat(Array.from(airplsResult.baseline)))
      // 校正
      correctedAoa.push([dataJsonArr[i].header].concat(Array.from(airplsResult.corrected)))
      // 拟合参数
      resultArgsAoa.push([
        dataJsonArr[i].header,
        airplsResult.iteration,
        airplsResult.error
      ])
    }
  }
  // 生成表格（先转置）
  // aoa_to_sheet()方法接受Array-of-Array对象，FLoat64Array不行
  // 如遇FLoat64Array，一定要提前用Array.from()转成普通Array
  const sheetBaselineAoa = XLSX.utils.aoa_to_sheet(arrayTranspose(baselineAoa))
  const sheetCorrectedAoa = XLSX.utils.aoa_to_sheet(arrayTranspose(correctedAoa))
  const sheetResultArgsAoa = XLSX.utils.aoa_to_sheet(resultArgsAoa)
  // 创建一个新的工作簿对象
  const workbook = XLSX.utils.book_new()
  // 将工作表添加到工作簿，该表名为"波谱数据集"
  XLSX.utils.book_append_sheet(workbook, sheetCorrectedAoa, "波谱数据集_去基线")
  XLSX.utils.book_append_sheet(workbook, sheetBaselineAoa, "波谱数据集_基线")
  XLSX.utils.book_append_sheet(workbook, sheetResultArgsAoa, "波谱数据集_参数")
  // 以工作簿生成ArrayBuffer对象
  const workbookArrBufObj = XLSX.write(workbook, { type: "buffer" })
  // 以xlsx格式下载该工作簿ArrayBuffer对象
  downloadFile(workbookArrBufObj, "波谱数据集_去基线.xlsx")
  // 隐藏加载圈
  uni.hideLoading()
} catch (error) {
  console.error("resultToDownload()报错: ", error)
  uni.hideLoading()
  uni.showModal({
    showCancel: false,
    title: "数据导出报错",
    content: error,
  })
}}

</script>
